package com.kevin.netty.starter;

import com.kevin.netty.starter.annotation.NettyHandler;
import com.kevin.netty.starter.enums.ProtocolEnum;
import com.kevin.netty.starter.preperties.ServerProperties;
import com.kevin.netty.starter.util.SSLContextUtil;
import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.epoll.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.ThreadFactory;

@Slf4j
@Getter
@Setter
@ToString
@Accessors(chain = true)
public class NettyServer {

    private SslContext sslContext;
    private AbstractBootstrap bootstrap;
    private ChannelFuture channelFuture;
    private ThreadFactory bossThreadFactory;
    private ThreadFactory workerThreadFactory;
    private EventLoopGroup bossEventLoopGroup;
    private EventLoopGroup workerEventLoopGroup;
    private Class<? extends Channel> channelType;
    private ChannelInitializer channelInitializer;
    private Map<ChannelOption<?>, Object> channelOptions = new LinkedHashMap<>();
    private Map<ChannelOption<?>, Object> childChannelOptions = new LinkedHashMap<>();
    private LinkedHashMap<String, ChannelHandlerAdapter> channelHandlerAdapterLinkedHashMap;

    private final ServerProperties properties;

    public NettyServer(ServerProperties properties, List<Object> beansWithNettyHandlerAnnotation) {
        this.properties = properties;
        this.channelHandlerAdapterLinkedHashMap = this.parseChannelHandler(beansWithNettyHandlerAnnotation);

        this.init();
    }

    public synchronized void init() {
        this.initEventLoopGroup();
        this.initChannelType();
        this.initChannelOptions();
        this.initSslContext();
        this.initSocketChannelInitializer();
        ProtocolEnum protocol = this.properties.getProtocol();
        switch (protocol) {
            case TCP:
                this.bootstrap = new ServerBootstrap();
                ServerBootstrap serverBootstrap = (ServerBootstrap) this.bootstrap;
                serverBootstrap.group(this.bossEventLoopGroup, this.workerEventLoopGroup);
                serverBootstrap.channel((Class<? extends ServerChannel>) this.channelType)
                        .handler(new LoggingHandler(this.properties.getLogLevel())).childHandler(this.channelInitializer);
                Iterator<ChannelOption<?>> iterator = this.channelOptions.keySet().iterator();

                ChannelOption childOpt;
                while (iterator.hasNext()) {
                    childOpt = iterator.next();
                    serverBootstrap.option(childOpt, this.channelOptions.get(childOpt));
                }

                iterator = this.childChannelOptions.keySet().iterator();
                while (iterator.hasNext()) {
                    childOpt = iterator.next();
                    serverBootstrap.childOption(childOpt, this.childChannelOptions.get(childOpt));
                }
                break;
            case UDP:
                this.bootstrap = new Bootstrap();
                this.bootstrap.group(this.bossEventLoopGroup);
                this.bootstrap.channel(this.channelType).handler(this.channelInitializer);
                for (ChannelOption<?> channelOption : this.channelOptions.keySet()) {
                    this.bootstrap.option(channelOption, this.channelOptions.get(channelOption));
                }
                break;
        }
        this.start();
        Runtime.getRuntime().addShutdownHook(new Thread(this::stop, this.properties.getName() + "-shutdownHook"));
    }

    public synchronized void start() {
        long startTime = System.currentTimeMillis();
        String host = this.properties.getHost();
        Integer port = this.properties.getPort();
        this.channelFuture = StringUtils.hasText(host) ?
                this.bootstrap.bind(host, port).awaitUninterruptibly() : this.bootstrap.bind(port).awaitUninterruptibly();
        if (null != this.channelFuture.cause()) {
            log.error("=== netty startup failed : {}", this.channelFuture.cause().getMessage(), this.channelFuture.cause());
        }
        if (this.channelFuture.isSuccess()) {
            log.debug("=== {} netty server started successfully , port : {} , time : {} ms ===",
                    this.properties.getName(), this.properties.getPort(), (System.currentTimeMillis() - startTime));
        }
    }

    public synchronized void stop() {
        if (null != this.channelFuture) {
            this.channelFuture.channel().closeFuture();
        }
        if (null != this.bossEventLoopGroup) {
            this.bossEventLoopGroup.shutdownGracefully();
        }
        if (null != this.workerEventLoopGroup) {
            this.workerEventLoopGroup.shutdownGracefully();
        }
        log.debug("=== {} netty server stopped successfully , port : {} ===", this.properties.getName(), this.properties.getPort());
    }

    private void initEventLoopGroup() {
        this.bossThreadFactory = new DefaultThreadFactory(this.properties.getName() + "-boss", true, Thread.NORM_PRIORITY);
        this.workerThreadFactory = new DefaultThreadFactory(this.properties.getName() + "-worker", true, Thread.NORM_PRIORITY);
        this.bossEventLoopGroup = this.buildBossEventLoopGroup();
        this.workerEventLoopGroup = this.buildWorkerEventLoopGroup();
    }

    private void initChannelType() {
        ProtocolEnum protocol = this.properties.getProtocol();
        switch (protocol) {
            case TCP:
                this.channelType = this.useNative() ? EpollServerSocketChannel.class : NioServerSocketChannel.class;
                break;
            case UDP:
                this.channelType = NioDatagramChannel.class;
                break;
        }
    }

    private void initChannelOptions() {
        this.channelOptions.put(ChannelOption.SO_BACKLOG, 1024);
        this.channelOptions.put(ChannelOption.SO_REUSEADDR, true);
        this.channelOptions.put(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        this.childChannelOptions.put(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        if (this.useNative()) {
            this.channelOptions.put(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED);
            this.childChannelOptions.put(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED);
        }
        ProtocolEnum protocol = this.properties.getProtocol();
        switch (protocol) {
            case TCP:
                this.childChannelOptions.put(ChannelOption.TCP_NODELAY, true);
                break;
            case UDP:
                break;
        }
    }

    public void initSslContext() {
        if (properties.getSsl().isEnabled()) {
            this.sslContext = SSLContextUtil.sslContext(properties.getSsl());
        }
    }

    private void initSocketChannelInitializer() {
        ProtocolEnum protocol = this.properties.getProtocol();
        switch (protocol) {
            case TCP:
                this.channelInitializer = new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) {
                        pipeline(ch.pipeline(), ch);
                    }
                };
                break;
            case UDP:
                this.channelInitializer = new ChannelInitializer<DatagramChannel>() {
                    protected void initChannel(DatagramChannel ch) {
                        pipeline(ch.pipeline(), ch);
                    }
                };
                break;
        }
    }

    private EventLoopGroup buildBossEventLoopGroup() {
        ProtocolEnum protocol = this.properties.getProtocol();
        return switch (protocol) {
            case TCP ->
                    this.useNative() ? new EpollEventLoopGroup(this.properties.getBossCount(), this.bossThreadFactory)
                            : new NioEventLoopGroup(this.properties.getBossCount(), this.bossThreadFactory);
            case UDP -> new NioEventLoopGroup(this.properties.getBossCount(), this.bossThreadFactory);
        };
    }

    private EventLoopGroup buildWorkerEventLoopGroup() {
        ProtocolEnum protocol = this.properties.getProtocol();
        return switch (protocol) {
            case TCP ->
                    this.useNative() ? new EpollEventLoopGroup(this.properties.getWorkerCount(), this.workerThreadFactory)
                            : new NioEventLoopGroup(this.properties.getWorkerCount(), this.workerThreadFactory);
            case UDP -> new NioEventLoopGroup(this.properties.getWorkerCount(), this.bossThreadFactory);
        };
    }

    private void pipeline(ChannelPipeline pipeline, Channel ch) {
        pipeline.channel().config().setWriteBufferLowWaterMark(properties.getLowWaterMark().intValue());
        pipeline.channel().config().setWriteBufferHighWaterMark(properties.getHighWaterMark().intValue());
        pipeline.channel().config().setOption(NioChannelOption.SO_SNDBUF, properties.getSndBuf().intValue());
        pipeline.channel().config().setOption(NioChannelOption.SO_RCVBUF, properties.getRcvBuf().intValue());
        if (null != this.sslContext) {
            pipeline.addLast(this.sslContext.newHandler(ch.alloc()));
        }
        pipeline.addLast("idleStateHandler", new IdleStateHandler(NettyServer.this.properties.getReaderIdleTime(),
                NettyServer.this.properties.getWriterIdleTime(), NettyServer.this.properties.getAllIdleTime()));
        if (!NettyServer.this.channelHandlerAdapterLinkedHashMap.isEmpty()) {
            NettyServer.this.channelHandlerAdapterLinkedHashMap.keySet().forEach((handlerName) -> {
                ChannelHandlerAdapter handler = NettyServer.this.channelHandlerAdapterLinkedHashMap.get(handlerName);
                if (handler.isSharable()) {
                    pipeline.addLast(handlerName, handler);
                } else {
                    try {
                        pipeline.addLast(handlerName, handler.getClass().newInstance());
                    } catch (IllegalAccessException | InstantiationException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

    private LinkedHashMap<String, ChannelHandlerAdapter> parseChannelHandler(List<Object> beansWithNettyHandlerAnnotation) {
        List<ChannelHandlerAdapter> channelHandlerAdapterList = beansWithNettyHandlerAnnotation.stream().filter((handlerBean) -> {
                    NettyHandler nettyHandler = AnnotationUtils.findAnnotation(handlerBean.getClass(), NettyHandler.class);
                    return nettyHandler != null && handlerBean instanceof ChannelHandlerAdapter && nettyHandler.name().equals(this.properties.getName());
                }).map((handlerBean) -> (ChannelHandlerAdapter) handlerBean)
                .sorted(Comparator.comparingInt((o) ->
                        Objects.requireNonNull(AnnotationUtils.findAnnotation(o.getClass(), NettyHandler.class)).order())).toList();
        LinkedHashMap<String, ChannelHandlerAdapter> channelHandlerAdapterLinkedHashMap = new LinkedHashMap<>();

        ChannelHandlerAdapter handlerAdapter;
        String handlerName;
        for (Iterator<ChannelHandlerAdapter> iterator = channelHandlerAdapterList.iterator();
             iterator.hasNext(); channelHandlerAdapterLinkedHashMap.put(handlerName, handlerAdapter)) {
            handlerAdapter = iterator.next();
            handlerName = handlerAdapter.getClass().getSimpleName();
            if (handlerName.contains("$$")) {
                handlerName = handlerName.substring(0, handlerName.indexOf("$$"));
            }
        }

        beansWithNettyHandlerAnnotation.removeAll(channelHandlerAdapterList);
        return channelHandlerAdapterLinkedHashMap;
    }

    private boolean useNative() {
        return Epoll.isAvailable();
    }

}

